(function() {
	// 游戏配置
	var config = {
		easy: {
			HERO_MISSILE_COUNT_MAX: 50,
			ENEMY_COUNT_MAX: 10,
			ENEMY_MISSILE_COUNT_MAX: 1,
			ENERY_CAN_COUNT_MAX: 3,
			COLLIDED_BY_ENEMY_COST: 10,
			CATCH_ENERGY_CAN_GAIN: 10,
			CATCH_AMMO_BOX_GAIN: 10,
			HIT_ENEMY_GAIN: 3,
			MISS_ENEMY_LOST: 1,
			INIT_SCORE: 50,
			SPEED_BASE: 1
		},
		medium: {
			HERO_MISSILE_COUNT_MAX: 30,
			ENEMY_COUNT_MAX: 20,
			ENEMY_MISSILE_COUNT_MAX: 3,
			ENERY_CAN_COUNT_MAX: 2,
			COLLIDED_BY_ENEMY_COST: 20,
			CATCH_ENERGY_CAN_GAIN: 10,
			CATCH_AMMO_BOX_GAIN: 8,
			HIT_ENEMY_GAIN: 2,
			MISS_ENEMY_LOST: 1,
			INIT_SCORE: 30,
			SPEED_BASE: 2
		},
		hard: {
			HERO_MISSILE_COUNT_MAX: 20,
			ENEMY_COUNT_MAX: 20,
			ENEMY_MISSILE_COUNT_MAX: 5,
			ENERY_CAN_COUNT_MAX: 2,
			COLLIDED_BY_ENEMY_COST: 30,
			CATCH_ENERGY_CAN_GAIN: 5,
			CATCH_AMMO_BOX_GAIN: 5,
			HIT_ENEMY_GAIN: 1,
			MISS_ENEMY_LOST: 1,
			INIT_SCORE: 20,
			SPEED_BASE: 3
		},
		GAME_DATA_KEY: "space_fighting_data"
	};

	/** 战场范围 */
	var Field = {
		width: 1200,
		height: 800
	};

	// 定义全局变量
	var autoid = 1;
	var explodes_at = [];

	// 定义全局函数

	/** 绘制爆炸效果 */
	function drawExplosion() {
		for (let i = 0; i < explodes_at.length; i++) {
			document.getElementById("explosions").innerHTML += `
				<div id="exp${autoid}" class="explode" style="left:${
				explodes_at[i].left
			}px;top:${explodes_at[i].top}px"></div>
			`;
			explodes_at.splice(i, 1);
			var id = "exp" + autoid;
			setTimeout(function() {
				document.getElementById(id).remove();
			}, 600);
			autoid++;
			Game.sounds.destroyed.currentTime = 0;
			Game.sounds.destroyed.play();
		}
		explodes_at = [];
	}

	// 事件定义

	var on_score_change = new CustomEvent("score_change", {
		detail: {}
	});
	var on_life_value_change = new CustomEvent("life_value_change", {
		detail: {}
	});
	var on_missile_count_change = new CustomEvent("missile_count_change", {
		detail: {}
	});

	// 事件监听

	document.addEventListener("score_change", function(e) {
		Game.hero.tune_score(e.detail.value);
	});

	document.addEventListener("life_value_change", function(e) {
		Game.hero.tune_life_value(e.detail.value);
	});
	document.addEventListener("missile_count_change", function(e) {
		Game.hero.tune_missile_count(e.detail.value);
	});

	// 键盘按键按下
	document.addEventListener("keydown", function(e) {
		if (e.key === "p" || e.key === "P") {
			if (e.target.id !== "player-name") {
				Game.paused = !Game.paused;
				Game.start();
			}
		}
		if (Game.is_over || Game.paused) {
			return;
		}
		// console.log(e);
		if (e.keyCode === 37) {
			// Left
			Game.hero.moving = true;
			Game.hero.dir = "left";
		}
		if (e.keyCode === 38) {
			// Up
			Game.hero.moving = true;
			Game.hero.dir = "up";
		}
		if (e.keyCode === 39) {
			// Right
			Game.hero.moving = true;
			Game.hero.dir = "right";
		}
		if (e.keyCode === 40) {
			// Left
			Game.hero.moving = true;
			Game.hero.dir = "down";
		}
		if (e.keyCode === 32) {
			// Spacebar (fire)
			Game.hero.fire();
		}
	});

	// 键盘按键释放
	document.addEventListener("keyup", function(e) {
		if (e.keyCode >= 37 && e.keyCode <= 40) {
			Game.hero.moving = false;
			Game.hero.dir = "";
		}
		if (e.keyCode === 32) {
			Game.hero.shooting = false;
		}
	});

	// 游戏开始画面 开始游戏按钮 点击
	document
		.getElementById("start-game-button")
		.addEventListener("click", function(e) {
			document.getElementById("start-screen").style.display = "none";
			document.getElementById("battle-field").style.display = "block";

			Game.start();
		});

	// 游戏开始画面 玩家昵称输入框 按键释放
	document.getElementById("player-name").addEventListener("keyup", function(e) {
		if (e.target.value != "") {
			if (Game.ready) {
				document.getElementById("start-game-button").disabled = false;
				Game.current_user = e.target.value;
				if (e.key == "Enter") {
					document.getElementById("start-game-button").click();
				}
			}
		} else {
			document.getElementById("start-game-button").disabled = true;
		}
	});
	// 游戏结束画面 重新开始按钮 点击
	document
		.getElementById("restart-game-button")
		.addEventListener("click", function() {
			Game.init();
		});

	// 游戏开始画面 选择游戏难度 难度改变
	document.getElementById("difficulty").addEventListener("change", function(e) {
		Game.cfg = config[e.target.value];
		Game.difficulty = e.target.options[e.target.selectedIndex].text;
		Game.init();
	});

	// 游戏开始画面 清除游戏数据按钮 点击
	document.getElementById("clear-data").addEventListener("click", function(e) {
		localStorage.removeItem(config.GAME_DATA_KEY);
		console.log(e.target);
		e.target.disabled = true;
	});

	// 相关对象与构造函数定义

	/** 帮助对象 */
	var H = {
		get_rand_drop_pos(obj_width, far_top = -3000) {
			return [
				Math.floor(Math.random() * (Field.width - obj_width)),
				Math.floor(Math.random() * far_top)
			];
		},
		collided(obj1, obj2) {
			return (
				obj1.left < obj2.left + obj2.width &&
				obj1.left + obj1.width > obj2.left &&
				obj1.top < obj2.top + obj2.height &&
				obj1.top + obj1.height > obj2.top
			);
		},
		q(id) {
			return document.getElementById(id);
		},
		qc(_class) {
			return document.getElementsByClassName(_class);
		}
	};

	/** 游戏角色集合类 */
	class RoleCollection {
		constructor() {
			this.roles = [];
		}
		*[Symbol.iterator]() {
			for (let role of this.roles) {
				yield role;
			}
		}
		append(role) {
			this.roles.push(role);
		}
		move() {
			for (let role of this.roles) {
				role.move();
			}
		}
		destroyDoneRoles() {
			for (let role of this.roles) {
				if (role.to_be_destroyed) {
					// 如果需要被摧毁
					if (role.el) {
						//删除 HTML 元素，如果有的话
						role.el.remove();
					}
				}
			}
			this.roles = this.roles.filter(r => !r.to_be_destroyed);
		}
	}

	class Role {
		constructor(
			width,
			height,
			left,
			top,
			speed,
			append_to,
			class_name,
			id_name
		) {
			this.width = width;
			this.height = height;
			this.speed = speed;
			this.el = null;
			this.append_to = append_to;
			this.class_name = class_name;
			this.id_name = id_name;
			this.to_be_destroyed = false;
			this.init_pos(left, top);
		}
		init_pos(left, top) {
			this.left = left;
			this.top = top;
			this.draw();
		}
		draw() {
			if (!this.el) {
				let el = null;
				if (this.id_name) {
					el = document.getElementById(this.id_name);
					if (!el) {
						el = document.createElement("div");
						el.id = this.id_name;
						if (this.class_name) {
							el.className = this.class_name;
						}
					}
				} else if (this.class_name) {
					el = document.createElement("div");
					el.className = this.class_name;
					if (this.id_name) {
						el.id = this.id_name;
					}
				}
				if (this.append_to) {
					let p = H.q(this.append_to);
					if (!p) {
						let msg = `ID 为 ${this.append_to} 的页面元素丢失！`;
						alert(msg);
						throw msg;
					}
					document.getElementById(this.append_to).append(el);
				}
				this.el = el;
			}
			// 定位到页面
			this.el.style.left = this.left + "px";
			this.el.style.top = this.top + "px";
		}
	}

	/** 玩家战机类 */
	class Hero extends Role {
		constructor(
			width,
			height,
			left,
			top,
			speed,
			append_to = "battle-field",
			class_name = "hero",
			id_name = "hero"
		) {
			super(width, height, left, top, speed, append_to, class_name, id_name);
			this.moving = false;
			this.dir = "";
			this.shooting = false;
			this.life_value = 100;
			this.missile_count = Game.cfg.HERO_MISSILE_COUNT_MAX;
			this.score = Game.cfg.INIT_SCORE;
		}
		move() {
			// 上下左右移动
			if (this.moving) {
				if (this.dir == "left" && !(this.left <= 0)) {
					this.left -= this.speed;
				} else if (
					this.dir == "right" &&
					!(this.left >= Field.width - this.width)
				) {
					this.left += this.speed;
				} else if (this.dir == "up" && !(this.top <= 0)) {
					this.top -= this.speed;
				} else if (
					this.dir == "down" &&
					!(this.top + this.height >= Field.height)
				) {
					this.top += this.speed;
				}

				this.draw();
			}
			// 与敌机相撞？
			for (let enemy of Game.enemies) {
				if (this.collided_by(enemy)) {
					this.el.style.animation = null;
					this.el.offsetHeight;
					this.el.style.animation = "hero-blink 0.2s 2";

					on_score_change.detail.value = Game.cfg.HIT_ENEMY_GAIN;
					document.dispatchEvent(on_score_change);
					// Or: this.tune_score(+Game.cfg.HIT_ENEMY_GAIN);

					on_life_value_change.detail.value = -Game.cfg.COLLIDED_BY_ENEMY_COST;
					document.dispatchEvent(on_life_value_change);
					// Or: this.tune_life_value(-Game.cfg.COLLIDED_BY_ENEMY_COST);

					explodes_at.push({
						left: enemy.left,
						top: enemy.top
					});
					enemy.init_pos(...H.get_rand_drop_pos(enemy.width));
				}
			}
			// 接住能量筒？
			for (let can of Game.energy_cans) {
				if (this.get_energy_can(can)) {
					this.tune_life_value(Game.cfg.CATCH_ENERGY_CAN_GAIN);
					can.init_pos(...H.get_rand_drop_pos(can.width));
					Game.sounds.charging.currentTime = 0;
					Game.sounds.charging.play();
				}
			}
			for (let ammo of Game.ammo_boxes) {
				if (this.get_ammo_box(ammo)) {
					this.tune_missile_count(Game.cfg.CATCH_AMMO_BOX_GAIN);
					ammo.init_pos(...H.get_rand_drop_pos(ammo.width));
					Game.sounds.reloading.currentTime = 0;
					Game.sounds.reloading.play();
				}
			}
		}
		/** 开火 */
		fire() {
			if (!this.shooting && this.missile_count > 0) {
				Game.hero_missiles.append(
					new HeroMissile(10, 28, this.left + 20, this.top - 20)
				);
				this.missile_count--;
				this.tune_missile_count(-1);
				Game.sounds.shoot.currentTime = 0;
				Game.sounds.shoot.play();
				this.shooting = true;
			}
		}
		collided_by(enemy) {
			return H.collided(this, enemy);
		}
		get_energy_can(energy_can) {
			return H.collided(this, energy_can);
		}
		get_ammo_box(ammo) {
			return H.collided(this, ammo);
		}
		tune_life_value(val) {
			this.life_value += val;
			if (this.life_value > 100) {
				this.life_value = 100;
			}
			if (this.life_value <= 0) {
				this.life_value = 0;
				Game.over();
			}
			document.getElementById("life-indicator").value = this.life_value;
			document.getElementById("life-indicator-text").innerText =
				this.life_value + "%";
		}
		tune_missile_count(count) {
			this.missile_count += count;
			if (this.missile_count > Game.cfg.HERO_MISSILE_COUNT_MAX) {
				this.missile_count = Game.cfg.HERO_MISSILE_COUNT_MAX;
			}
			if (this.missile_count < 0) {
				this.missile_count = 0;
			}
			document.getElementById("missile-count").innerText = this.missile_count;
		}
		tune_score(value) {
			this.score += value;
			document.getElementById("score").innerText =
				this.score < 0 ? 0 : this.score;
			if (this.score <= 0) {
				this.score = 0;
				Game.over();
			}
		}
	}

	/** 敌军战机类 */
	class Enemy extends Role {
		constructor(
			width,
			height,
			left,
			top,
			speed,
			append_to = "enemies",
			class_name = "enemy",
			id_name = null
		) {
			if (left === null && left === null) {
				[left, top] = H.get_rand_drop_pos(width);
			}
			super(width, height, left, top, speed, append_to, class_name, id_name);
		}
		move() {
			if (this.top < Field.height) {
				this.top += this.speed;
				// 被导弹击中？
				for (let missile of Game.hero_missiles) {
					if (this.hitted_by(missile)) {
						this.init_pos(...H.get_rand_drop_pos(this.width));
						missile.to_be_destroyed = true;
						on_score_change.detail.value = Game.cfg.HIT_ENEMY_GAIN;
						document.dispatchEvent(on_score_change);
						// 或者：Game.hero.tune_score(Game.cfg.HIT_ENEMY_GAIN);
						explodes_at.push({
							left: missile.left - 35,
							top: missile.top - 65
						});
					}
				}
				this.draw();
			} else {
				Game.hero.tune_score(-Game.cfg.MISS_ENEMY_LOST);
				this.init_pos(...H.get_rand_drop_pos(this.width));
			}
		}
		hitted_by(missile) {
			return H.collided(this, missile);
		}
	}

	/** 能量筒类 */
	class EnergyCan extends Role {
		constructor(
			width,
			height,
			left,
			top,
			speed,
			append_to = "energy-cans",
			class_name = "energy-can",
			id_name = null
		) {
			if (left === null && left === null) {
				[left, top] = H.get_rand_drop_pos(width);
			}
			super(width, height, left, top, speed, append_to, class_name, id_name);
		}
		move() {
			if (this.top > Field.height) {
				this.init_pos(...H.get_rand_drop_pos(this.width));
			} else {
				this.top += this.speed;
			}
			this.draw();
		}
	}

	/** 弹药箱类 */
	class AmmoBox extends Role {
		constructor(
			width,
			height,
			left,
			top,
			speed,
			append_to = "ammo-boxes",
			class_name = "ammo-box",
			id_name = null
		) {
			if (left === null && left === null) {
				[left, top] = H.get_rand_drop_pos(width);
			}
			super(width, height, left, top, speed, append_to, class_name, id_name);
		}
		move() {
			if (this.top > Field.height) {
				this.init_pos(...H.get_rand_drop_pos(this.width));
			} else {
				this.top += this.speed;
			}
			this.draw();
		}
	}

	/** 玩家战机导弹类 */
	class HeroMissile extends Role {
		constructor(
			width,
			height,
			left,
			top,
			speed = 8,
			append_to = "hero-missiles",
			class_name = "hero-missile",
			id_name = null
		) {
			super(width, height, left, top, speed, append_to, class_name, id_name);
			this.done = false;
		}

		move() {
			if (this.top >= 0 - this.height) {
				this.top -= this.speed;
				this.draw();
			} else {
				this.to_be_destroyed = true;
			}
		}
	}

	/** 游戏类 */
	var Game = {
		cfg: config.easy,
		difficulty: "容易",
		sounds: {},
		sounds_loaded: 0,
		intv: null,
		ready: false,
		current_user: "",
		hero: null,
		enemies: null,
		energy_cans: null,
		ammo_boxes: null,
		hero_missiles: null,
		paused: false,
		is_over: false,
		init() {
			Game.paused = false;
			Game.is_over = false;
			Game.hero = new Hero(50, 50, 575, 745, 5);
			Game.init_info();
			H.q("start-screen").style.display = "flex";
			H.q("battle-field").style.display = "none";
			H.q("game-over").style.display = "none";
			for (let el of H.qc("need-erase")) {
				el.innerHTML = "";
			}
			H.q("clear-data").disabled =
				localStorage.getItem(config.GAME_DATA_KEY) === null;

			Game.hero_missiles = new RoleCollection();

			Game.enemies = new RoleCollection();
			for (let i = 0; i < Game.cfg.ENEMY_COUNT_MAX; i++) {
				Game.enemies.append(new Enemy(50, 60, null, null, 3));
			}
			Game.energy_cans = new RoleCollection();

			for (let i = 0; i < Game.cfg.ENERY_CAN_COUNT_MAX; i++) {
				Game.energy_cans.append(new EnergyCan(20, 32, null, null, 4));
			}
			Game.ammo_boxes = new RoleCollection();

			for (let i = 0; i < Game.cfg.ENERY_CAN_COUNT_MAX; i++) {
				Game.ammo_boxes.append(new AmmoBox(40, 21, null, null, 4));
			}

			let sound_names = [
				"background",
				"shoot",
				"destroyed",
				"charging",
				"reloading"
			];
			for (let sound_name of sound_names) {
				let sound = new Audio();
				if (sound_name == "background") {
					sound.loop = true;
				}
				sound.addEventListener("canplaythrough", function(e) {
					Game.sounds[sound_name] = sound;
					Game.sounds_loaded++;
					if (Game.sounds_loaded == sound_names.length) {
						Game.ready = true;
					}
				});
				sound.src = `sound/${sound_name}.mp3`;
			}
			let instruction = `
				<li>1. 每击中一架敌机 <img src="images/enemy.png" style="width: 30px;height: auto"> 得 ${
					Game.cfg.HIT_ENEMY_GAIN
				} 分；</li>
				<li>2. 每逃走一架敌机 <img src="images/enemy.png" style="width: 30px;height: auto"> 失 ${
					Game.cfg.MISS_ENEMY_LOST
				} 分；</li>
				<li>3. 与敌机 <img src="images/enemy.png" style="width: 30px;height: auto"> 相撞得 ${
					Game.cfg.HIT_ENEMY_GAIN
				} 分，失去 ${Game.cfg.COLLIDED_BY_ENEMY_COST}% 生命值；</li>
				<li>4. 接收到一个能量筒 <img src="images/energy-can.png" style="width: 20px;height: auto"> 恢复 ${
					Game.cfg.CATCH_ENERGY_CAN_GAIN
				}% 生命力；</li>
				<li>5. 装弹量 50，每接收到一个弹药箱 <img src="images/ammo-box.png" style="height: 20px;width: auto"> ，得 ${
					Game.cfg.CATCH_AMMO_BOX_GAIN
				} 枚导弹；</li>
				<li>6. 得分为 0 或者生命值用完，游戏结束。</li>
			`;
			document.getElementById("instruction").innerHTML = instruction;

			// 动态更换背景
			let bkgno = Math.floor(1 + Math.random() * (6 - 1 + 1));
			document.getElementById("battle-field").style.backgroundImage =
				"url(../images/background" + bkgno + ".jpg)";
			document.getElementById("start-screen").style.backgroundImage =
				"url(../images/background" + bkgno + ".jpg)";
		},

		init_info() {
			document.getElementById("score").innerText = Game.cfg.INIT_SCORE;
			document.getElementById("missile-count").innerText =
				Game.cfg.HERO_MISSILE_COUNT_MAX;
			H.q("difficulty-text").innerText = Game.difficulty;
			H.q("life-indicator").value = Game.hero.life_value;
			H.q("life-indicator-text").innerText = Game.hero.life_value + "%";
		},

		start() {
			Game.hero.draw();
			Game.sounds.background.play();
			var fps = 60;
			var now;
			var then = Date.now();
			var interval = 1000 / fps;
			var delta;

			function loop() {
				if (!Game.is_over && !Game.paused) {
					Game.intv = requestAnimationFrame(loop);
					now = Date.now();
					delta = now - then;

					if (delta > interval) {
						// update time stuffs

						// Just `then = now` is not enough.
						// Lets say we set fps at 10 which means
						// each frame must take 100ms
						// Now frame executes in 16ms (60fps) so
						// the loop iterates 7 times (16*7 = 112ms) until
						// delta > interval === true
						// Eventually this lowers down the FPS as
						// 112*10 = 1120ms (NOT 1000ms).
						// So we have to get rid of that extra 12ms
						// by subtracting delta (112) % interval (100).
						// Hope that makes sense.

						then = now - (delta % interval);

						// ... Code for Drawing the Frame ...
						Game.hero.move();
						Game.enemies.move();
						Game.energy_cans.move();
						Game.ammo_boxes.move();
						Game.hero_missiles.move();
						Game.hero_missiles.destroyDoneRoles();
						drawExplosion();
					}
				} else {
					if (Game.is_over) {
						cancelAnimationFrame(Game.intv);
					}
					if (Game.paused) {
						Game.sounds.background.pause();
					}
				}
			}

			loop();
		},

		over() {
			Game.is_over = true;
			for (let key in Game.sounds) {
				if (Game.sounds.hasOwnProperty(key)) {
					Game.sounds[key].pause();
				}
			}
			document.getElementById("game-over").style.display = "flex";
			let data = JSON.parse(localStorage.getItem(config.GAME_DATA_KEY)) || [];
			data.push({
				player: Game.current_user,
				difficulty: Game.difficulty,
				score: Game.hero.score
			});
			data = data.filter(e => e.difficulty == Game.difficulty);
			data.sort((a, b) => (a.score > b.score ? -1 : 1));
			document.getElementById("rank").innerHTML = "";
			for (let i = 0; i < data.length; i++) {
				let self_found =
					data[i].player == Game.current_user &&
					data[i].score == Game.hero.score;
				let hlight = self_found ? "hlight" : "";
				if (i < 3 || (i >= 3 && self_found)) {
					document.getElementById("rank").innerHTML += `
				<tr class=${hlight}>
					<td>${i + 1}</td>
					<td>${data[i].player}</td>
					<td>${data[i].difficulty}</td>
					<td>${data[i].score}</td>
				</tr>
				`;
				}
				if (self_found && i >= 3) {
					break;
				}
			}
			localStorage.setItem(config.GAME_DATA_KEY, JSON.stringify(data));
		}
	};

	Game.init();
})();
